/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.util.core;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.Globals;
import org.apache.struts.util.TokenProcessor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Iterator;

/**
 * <p>This class provides functionality to restrict access to actions if the http-session is over or some action is called from wrong page </p>
 * <p/>
 * <p><a href="TokenUtil.java.html"><i>View Source</i></a></p>
 *
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @version $Revision: 1.2 $ $Date: 2006/03/16 11:09:44 $
 */
public class TokenUtil {

    /**
     * Key to store HashMap in the session with key: String token scope and value: String token
     */
    public static final String TOKENS_KEY = "com.blandware.atleap.webapp.action.core.TOKENS";

    /**
     * Commons Logging instance.
     */
    protected transient final Log log = LogFactory.getLog(TokenUtil.class);

    /**
     * The singleton instance of this class.
     */
    private static TokenUtil instance = new TokenUtil();

    /**
     * Retrieves the singleton instance of this class.
     *
     * @return singleton instance
     */
    public static TokenUtil getInstance() {
        return instance;
    }

    /**
     * Protected constructor for TokenUtil.  Use TokenUtil.getInstance()
     * to obtain a reference to this class.
     */
    protected TokenUtil() {
        super();
    }

    /**
     * <p>Determines token scope basing on current performed action path
     *
     * @param request request
     * @return the action path from the beginning to the last slash
     */
    protected String getDefaultTokenScope(HttpServletRequest request) {
        ActionMapping mapping = (ActionMapping) request.getAttribute(Globals.MAPPING_KEY);
        String path = mapping.getPath();
        int slash = path.lastIndexOf("/");
        if (slash != -1) {
            return path.substring(0, slash + 1);
        } else {
            return path;
        }
    }

    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in all scopes beginning from default, and the
     * value submitted as a request parameter with this action matches it.
     * Returns <code>false</code> under any of the following circumstances:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param request The servlet request we are processing
     * @param reset   Should we reset the token after checking it?
     * @return whether token exists and valid
     */
    public synchronized boolean isTokenValid(HttpServletRequest request, boolean reset) {
        return isTokenValid(null, request, reset);
    }

    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in all scopes beginning from default, and the
     * value submitted as a request parameter with this action matches it.
     * Returns <code>false</code> under any of the following circumstances:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param request The servlet request we are processing
     * @return whether token exists and valid
     */
    public synchronized boolean isTokenValid(HttpServletRequest request) {
        return isTokenValid(null, request, false);
    }


    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in specified scopes, and the value submitted
     * as a request parameter with this action matches it. Returns
     * <code>false</code> under any of the following circumstances:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param scopes  array of scope names, if null check all scopes begin from default
     * @param request The servlet request we are processing
     * @return whether token exists and valid
     */
    public synchronized boolean isTokenValid(String[] scopes, HttpServletRequest request) {
        return isTokenValid(scopes, request, false);
    }

    /**
     * <p>Returns <code>true</code> if there is a transaction token stored in
     * the user's current session in specified scopes, and the value submitted
     * as a request parameter with this action matches it. Returns
     * <code>false</code> if:</p>
     * <ul>
     * <li>No session associated with this request</li>
     * <li>No transaction token saved in the session</li>
     * <li>No transaction token included as a request parameter</li>
     * <li>The included transaction token value does not match the
     * transaction token in the user's session</li>
     * </ul>
     *
     * @param scopes  array of scope names, if null check all scopes begin from default
     * @param request The servlet request we are processing
     * @param reset   Should we reset the token after checking it?
     * @return whether token exists and valid
     */
    public synchronized boolean isTokenValid(String[] scopes, HttpServletRequest request, boolean reset) {
        // Retrieve the current session for this request
        HttpSession session = request.getSession(false);
        if (session == null) {
            return false;
        }

        // Retrieve the transaction token included in this request
        String requestToken = request.getParameter(org.apache.struts.taglib.html.Constants.TOKEN_KEY);
        if (requestToken == null) {
            return false;
        }

        // Retrieve the transaction token from this session
        String sessionToken = (String) session.getAttribute(Globals.TRANSACTION_TOKEN_KEY);

        if (requestToken.equals(sessionToken)) {
            if (reset) {
                resetTokenByToken(requestToken, request);
            }
            return true;
        }

        HashMap tokens = (HashMap) session.getAttribute(TOKENS_KEY);
        if (tokens == null) {
            tokens = new HashMap();
        }

        if (scopes != null && scopes.length > 0) {
            //check only in specified scopes
            for (int i = 0; i < scopes.length; i++) {
                String scope = scopes[i];
                String scopeToken = (String) tokens.get(scope);
                if (requestToken.equals(scopeToken)) {
                    if (reset) {
                        resetToken(scope, request);
                    }
                    return true;
                }
            }
        } else {
            //check in all scopes begin from default

            //check default
            String defaultScope = getDefaultTokenScope(request);
            String token = (String) tokens.get(defaultScope);
            if (requestToken.equals(token)) {
                if (reset) {
                    resetToken(defaultScope, request);
                }
                return true;
            }

            //check all
            for (Iterator iterator = tokens.keySet().iterator(); iterator.hasNext();) {
                String scope = (String) iterator.next();
                if (requestToken.equals(tokens.get(scope))) {
                    if (reset) {
                        resetToken(scope, request);
                    }
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * <p>Resets the saved transaction token in the user's session. This
     * indicates that transactional token checking will not be needed
     * on the next request that is submitted.</p>
     *
     * @param tokenScope name of token scope
     * @param request    The servlet request we are processing
     */
    public synchronized void resetToken(String tokenScope, HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        if (tokenScope == null) {
            tokenScope = getDefaultTokenScope(request);
        }
        session.removeAttribute(Globals.TRANSACTION_TOKEN_KEY);
        HashMap tokens = (HashMap) session.getAttribute(TOKENS_KEY);
        if (tokens == null) {
            tokens = new HashMap();
        }
        tokens.remove(tokenScope);
        session.setAttribute(TOKENS_KEY, tokens);
    }

    /**
     * <p>Resets the saved transaction token in the user's session. This
     * indicates that transactional token checking will not be needed
     * on the next request that is submitted.</p>
     *
     * @param token   token value
     * @param request The servlet request we are processing
     */
    protected synchronized void resetTokenByToken(String token, HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(Globals.TRANSACTION_TOKEN_KEY);
        HashMap tokens = (HashMap) session.getAttribute(TOKENS_KEY);
        if (tokens == null) {
            tokens = new HashMap();
        }

        String origScope = null;
        for (Iterator iterator = tokens.keySet().iterator(); iterator.hasNext();) {
            String scope = (String) iterator.next();
            if (token.equals(tokens.get(scope))) {
                origScope = scope;
                break;
            }
        }
        if (origScope != null) {
            tokens.remove(origScope);
            session.setAttribute(TOKENS_KEY, tokens);
        }
    }


    /**
     * <p>Resets the saved transaction token in the user's session. This
     * indicates that transactional token checking will not be needed
     * on the next request that is submitted.</p>
     *
     * @param request The servlet request we are processing
     */
    public synchronized void resetToken(HttpServletRequest request) {
        resetToken(null, request);
    }

    /**
     * <p>Saves a new transaction token in the user's current session, creating
     * a new session if necessary.</p>
     *
     * @param tokenScope name of token scope, if null uses default
     * @param request    The servlet request we are processing
     */
    public synchronized void saveToken(String tokenScope, HttpServletRequest request) {
        String token = TokenProcessor.getInstance().generateToken(request);
        if (token != null) {
            if (tokenScope == null) {
                tokenScope = getDefaultTokenScope(request);
            }

            HttpSession session = request.getSession();
            session.setAttribute(Globals.TRANSACTION_TOKEN_KEY, token);

            HashMap tokens = (HashMap) session.getAttribute(TOKENS_KEY);
            if (tokens == null) {
                tokens = new HashMap();
            }
            tokens.put(tokenScope, token);
            session.setAttribute(TOKENS_KEY, tokens);
        }
    }

    /**
     * <p>Saves a new transaction token in the user's current session, creating
     * a new session if necessary.</p>
     *
     * @param request The servlet request we are processing
     */
    public synchronized void saveToken(HttpServletRequest request) {
        saveToken(null, request);
    }

}
